Kernel(内核)
在 GPU 上执行的函数,是 GPU 编程的基本执行单元。算子是逻辑概念,Kernel 是算子的具体实现。
算子融合(Operator Fusion)
将多个 Kernel 合并成一个,减少显存读写次数,让中间结果留在寄存器/共享内存中。
容量更小
容量更大
算子融合与硬件的关系
算子融合是软件层面的优化策略,Tensor Core / CUDA Core是硬件执行单元。融合后的 Kernel 内部会协调使用两种硬件。
__global__ void fused_gemm_bias_gelu(...)中间结果在寄存器传递,不写回显存
硬件级融合 vs 软件级融合
| 层次 | 名称 | 示例 | 执行单元 |
|---|---|---|---|
| 硬件级 | FMA (Fused Multiply-Add) | D = A×B + C(单条指令) | Tensor Core |
| 软件级 | 算子融合 (Operator Fusion) | GEMM + Bias + GeLU → 1个 Kernel | Tensor Core + CUDA Core |
常用术语全称
| 缩写 | 全称 | 层次 | 说明 |
|---|---|---|---|
| FMA | Fused Multiply-Add | 指令级 | 单条硬件指令完成 D = A×B + C |
| GEMM | General Matrix Multiply | 算子级 | 通用矩阵乘法,由大量 FMA 操作组成 |
| GeLU | Gaussian Error Linear Unit | 算子级 | 高斯误差线性单元,Transformer 常用激活函数 |
| SiLU | Sigmoid Linear Unit | 算子级 | 又称 Swish,SiLU(x) = x × σ(x) |
| HBM | High Bandwidth Memory | 硬件 | 高带宽显存,现代 GPU 使用 |
| SM | Streaming Multiprocessor | 硬件 | 流式多处理器,GPU 基本计算单元 |
层次关系:FMA 是硬件指令(砖头),GEMM 是由大量 FMA 组成的算子(墙),算子融合是将多个算子合并成一个 Kernel(房子)。
什么是 Kernel?
Kernel 是在 GPU 上执行的函数,由 CPU 发起调用,在 GPU 的成千上万个线程上并行执行。
// 这就是一个 Kernel —— 使用 __global__ 关键字声明 __global__ void add(float *a, float *b, float *c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) c[i] = a[i] + b[i]; } // CPU 调用 Kernel,在 GPU 上并行执行 add<<>>(a, b, c, n);
CUDA 函数类型
| 标识符 | 执行位置 | 调用者 | 说明 |
|---|---|---|---|
__global__ |
GPU | CPU | Kernel 函数,GPU 并行执行 |
__device__ |
GPU | GPU | 设备函数,只能被 Kernel 调用 |
__host__ |
CPU | CPU | 普通 CPU 函数(默认) |
Kernel vs 算子
| 概念 | 层次 | 示例 |
|---|---|---|
| 算子 (Operator) | 逻辑概念 | MatMul, ReLU, LayerNorm, Softmax |
| Kernel | 具体实现 | __global__ void matmul_kernel(...) |
一个算子可能由一个或多个 Kernel 实现。例如,复杂的 MatMul 可能拆分为多个 Kernel 处理不同的分块。
GPU 存储层次详解
| 存储类型 | 容量 | 延迟 | 带宽 | 作用域 | 程序员可控 |
|---|---|---|---|---|---|
| 寄存器 | ~256KB/SM | 1 cycle | 最高 | 线程私有 | 隐式(局部变量) |
| 共享内存 | 128-256KB/SM | ~20 cycles | ~19 TB/s | Block 内共享 | ✅ __shared__ |
| L1 缓存 | 与共享内存共用 | ~30 cycles | ~19 TB/s | SM 内 | 部分可控 |
| L2 缓存 | 40-60MB | ~200 cycles | ~5 TB/s | 全 GPU | 自动 |
| 显存 (HBM) | 24-80GB | ~400 cycles | ~3 TB/s | 全 GPU | ✅ 显式分配 |
| 主机内存 | 几百GB | 极高 | ~64 GB/s | CPU 端 | ✅ |
Kernel 执行时的数据流动
Global Memory
加载数据
计算
结果暂存
写回结果
线程间共享
线程协作
关键区别:
• 寄存器:线程私有,用于暂存单线程的计算结果
• 共享内存:Block 内共享,用于线程间数据交换(如规约、分块加载)
• 单线程独立计算时:结果直接存寄存器,不经过共享内存
__global__ void example(float *input, float *output) { // 1. 声明共享内存(Block 内线程共享) __shared__ float tile[256]; // 2. 从显存读取到共享内存 tile[threadIdx.x] = input[blockIdx.x * 256 + threadIdx.x]; __syncthreads(); // 同步,确保数据加载完成 // 3. 从共享内存读到寄存器,进行计算 float val = tile[threadIdx.x]; // val 在寄存器中 val = val * 2.0f + 1.0f; // 计算在寄存器中完成 // 4. 结果写回显存 output[blockIdx.x * 256 + threadIdx.x] = val; }
规约操作(Reduction)
规约是一种将多个数据通过某种操作合并成一个值的计算模式,在深度学习中非常常见。
如:求和
常见的规约操作
| 规约类型 | 操作 | 示例 |
|---|---|---|
| Sum | 求和 | [1, 2, 3, 4] → 10 |
| Max | 求最大值 | [1, 5, 3, 2] → 5 |
| Min | 求最小值 | [4, 2, 6, 1] → 1 |
| Product | 求积 | [2, 3, 4] → 24 |
| Mean | 求均值 | [2, 4, 6, 8] → 5 |
| LogSumExp | Softmax 中用 | log(Σexp(xᵢ)) |
深度学习中的规约
# LayerNorm 中的规约 mean = x.mean(dim=-1) # 规约:求均值 var = x.var(dim=-1) # 规约:求方差 # Softmax 中的规约 max_val = x.max(dim=-1) # 规约:求最大值(数值稳定性) sum_exp = exp(x - max_val).sum(dim=-1) # 规约:求和 # Loss 计算 loss = losses.mean() # 规约:batch 内求均值 # Attention attn_weights = softmax(scores, dim=-1) # 内含 max 和 sum 规约
规约为什么需要线程协作?
因为数据分布在不同线程中,要得到全局结果必须线程间通信,这就需要用到共享内存。
GPU 并行规约代码
__shared__ float sdata[256]; // 共享内存,用于线程间通信 // 1. 每个线程加载自己的数据到共享内存 sdata[tid] = input[i]; __syncthreads(); // 同步,确保所有线程加载完成 // 2. 并行规约 - 树形结构,每轮活跃线程减半 for (int s = blockDim.x / 2; s > 0; s >>= 1) { if (tid < s) { sdata[tid] += sdata[tid + s]; // 读取其他线程的数据并累加 } __syncthreads(); // 每轮都要同步 } // 3. 线程0持有最终结果,写回显存 if (tid == 0) output[blockIdx.x] = sdata[0];
概念区分:规约 vs 协作
| 概念 | 含义 | 关系 |
|---|---|---|
| 规约 (Reduction) | 多个值 → 一个值的计算模式 | 算法/计算模式("做什么") |
| 协作 (Cooperation) | 多个线程共同完成任务 | 实现方式("怎么做") |
| 共享内存 | Block 内线程可共同访问的存储 | 协作的工具 |
| __syncthreads() | 等待所有线程到达同一点 | 协作的保障 |
总结:规约是"做什么"(计算模式),协作是"怎么做"(实现方式)。规约通常需要协作来实现,但协作不仅仅用于规约——比如分块矩阵乘法也需要协作加载数据到共享内存。
为什么显存访问是瓶颈?
计算/访存比分析
计算 1 次 FP16 乘加需要读取 4 字节
理论比值:1979T ÷ (3.35T/4) ≈ 2400:1
如果每次计算都访问显存,99.9% 的时间在等数据!
优化核心原则
✅ 减少显存访问次数
✅ 数据尽量留在寄存器/共享内存
✅ 多个操作合并到一个 Kernel
✅ 提高计算密度(每次访存做更多计算)
算子融合代码示例
// ❌ 未融合:两个独立 Kernel,两次显存读写 __global__ void add(float *a, float *b, float *c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) c[i] = a[i] + b[i]; } __global__ void relu(float *x, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) x[i] = x[i] > 0 ? x[i] : 0; } // 调用两次 Kernel add<<>>(a, b, c, n); // 写 c 到显存 relu<< >>(c, n); // 读 c,再写回
// ✅ 融合后:一个 Kernel,一次显存写入 __global__ void add_relu(float *a, float *b, float *c, int n) { int i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n) { float sum = a[i] + b[i]; // sum 在寄存器中 c[i] = sum > 0 ? sum : 0; // 直接计算后写出 } } // 只调用一次 Kernel add_relu<<>>(a, b, c, n);
深度学习中的典型融合模式
# PyTorch 原始写法(3个独立算子) x = conv(input) # 卷积: 读 input, 写 x x = batch_norm(x) # 归一化: 读 x, 写 x x = relu(x) # 激活: 读 x, 写 x # 融合后(1个融合算子) x = conv_bn_relu(input) # 读 input, 写 x # 中间结果在寄存器中传递
常见融合模式
| 融合模式 | 算子组合 | 应用场景 |
|---|---|---|
| Conv-BN-ReLU | 卷积 + 归一化 + 激活 | CNN |
| GEMM-Bias-Act | 矩阵乘 + 偏置 + 激活 | Transformer MLP |
| LayerNorm-QKV | 归一化 + 线性投影 | Attention |
| Softmax-Mask-Scale | 缩放 + 掩码 + Softmax | Attention |
| Add-LayerNorm | 残差连接 + 归一化 | Transformer |
算子优化工具与加速库
TensorRT
NVIDIA 推理优化器,自动算子融合
XLA
TensorFlow/JAX 编译器
TorchScript
PyTorch JIT 编译
Triton
OpenAI 开源,Python 写 Kernel
CUTLASS
NVIDIA CUDA 模板库
FlashAttention
Attention 算子深度优化
加速库 = 优化算子的集合
| 加速库 | 提供的算子 | 领域 |
|---|---|---|
| cuBLAS | GEMM, GEMV, DOT, AXPY... | 线性代数 |
| cuDNN | Conv, Pool, BN, ReLU, Attention... | 深度学习 |
| cuFFT | FFT, IFFT... | 傅里叶变换 |
| cuSPARSE | SpMV, SpMM... | 稀疏矩阵 |
torch.mm(A, B)cublasSgemm(...)__global__ void sgemm_optimized(...)